<?php

/**
 * Класс реализующий ORM.
 * От него должны быть унаследованы все классы объектов, хранящихся в БД
 */
abstract class Row
{
	protected $id;              // id объекта
	protected $props = null;    // массив свойств объекта (соответствует значениям атрибутов в БД)

	public static $props_cache = array();   // кэш класса, чтобы по возможности сократить количество обращений к БД
	
	/**
	 * Создание объекта.
	 * @param  $id
	 * @param  $props
	 * @return void
	 */
	function __construct($id, $props = null)
	{
		$this->id = '' . $id;
		if (!is_null($props)) $this->props = $props;
	}
	
	/**
	 * Получение значения конкретного свойства.
	 * @param  $key
	 * @return string
	 */
	function __get($key)
	{
		if ($key == 'id') return $this->id;

		$this->LoadProps();
		if (!isset($this->props[$key])) return NULL;

		return $this->props[$key];
	}
	
	/**
	 * Установка значения свойства объекта
	 * @param  $key
	 * @param  $value
	 * @return
	 */
	function __set($key, $value)
	{
		if (!is_null($this->props) && isset($this->props[$key]) && ($this->props[$key] == $value)) return;

		$class = get_called_class();
		$tbl = $class::TABLE;
		$table = self::GetTable($tbl);
		
		if (array_key_exists($key, $table['fields']))
		{
			$cleanfields = $this->GetStripTagsFields();
	
			if (array_search($key, $cleanfields) !== FALSE)
			{
				$value = strip_tags($value);
			}

			SQL::wquery("UPDATE $tbl SET `$key`=? WHERE id=?", $value, $this->id);
	
			if (!isset(Row::$props_cache[get_class($this)])) Row::$props_cache[get_class($this)] = array();
			unset(Row::$props_cache[get_class($this)][$this->id]);
		}

		$this->props[$key] = $value;
	}
	
	function __toString()
	{
		return ''.$this->id;
	}

	// названия полей, в которых нужно вырезать html
	public function GetStripTagsFields()
	{
		return array();
	}

	/**
	 * Статическое создание объекта.
	 * Обычно требуется для построения цепочек.
	 * @static
	 * @param  $id
	 * @param  $props
	 * @return
	 */
	static function Factory($id, $props = null)
	{
		$class = get_called_class();

		return new $class($id, $props);
	}

	/**
	 * По массиву айдишников строит массив объектов.
	 * @static
	 * @param  $ids
	 * @return
	 */
	static function MassFactory($ids)
	{
		foreach ($ids as &$id)
		{
			$id = self::Factory($id);
		}

		return $ids;
	}

	/**
	 * Загружает значения свойств объекта из БД
	 * @return
	 */
	protected function LoadProps()
	{
		if (!is_null($this->props)) return;

		if (!isset(Row::$props_cache[get_class($this)]))
		{
			Row::$props_cache[get_class($this)] = array();
		}

		if (isset(Row::$props_cache[get_class($this)][$this->id]))
		{
			$this->props = Row::$props_cache[get_class($this)][$this->id];
			return;
		}

		$class = get_called_class();
		$tbl = $class::TABLE;
		$res = SQL::hash_from_query("SELECT * FROM $tbl WHERE id=? LIMIT 1", $this->id);

		if ($res)
		{
			Row::$props_cache[get_class($this)][$this->id] = $this->props = $res;
		}
	}

	/**
	 * Заполнение свойств объекта.
	 * @param  $props
	 * @param bool $create
	 * @return
	 */
	function _FillProps($props, $create = true)
	{
		if (is_null($props)) return;

		if (is_null($this->props))
		{
			if (!$create) return;
			$this->props = array();
		}

		foreach ($props as $key => $value)
		{
			$this->props[$key] = $value;
		}
	}

	/**
	 * Установка значения свойств объекта.
	 * @param  $props
	 * @return void
	 */
	function SetProps($props)
	{
		$class	= get_called_class();
		$tbl	= $class::TABLE;
		$table	= self::GetTable($tbl);
		
		$prs	= array_intersect_key($props, $table['fields']);
		$cleanfields = $this->GetStripTagsFields();

		foreach ($cleanfields as $f)
		{
			if (isset($prs[$f])) $prs[$f] = strip_tags($prs[$f]);
		}

		SQL::wquery("UPDATE $tbl SET ?h WHERE id=?", $prs, $this->id);
		
		if (!is_null($this->prs))
		{
			$this->_FillProps($prs, true);
		}

		if (!isset(Row::$props_cache[get_class($this)])) Row::$props_cache[get_class($this)] = array();
		unset(Row::$props_cache[get_class($this)][$this->id]);
	}

	/**
	 * Получение структуры таблицы.
	 * @static
	 * @param  $tableName
	 * @return
	 */
	public static function GetTable($tableName)
	{
		static $tables;

		if(empty($tables[$tableName]))
		{
			$res	= SQL::wquery("SHOW COLUMNS FROM `{$tableName}`");
			
			if ($res)
			{
				while($row = $res->fetch_hash())
				{
					$tables[$tableName]['fields'][$row['Field']]	= $row['Field'];
					$tables[$tableName]['fieldsInfo'][$row['Field']]= $row;
				}
			}
		}

		return $tables[$tableName];
	}

	/**
	 * Получение свойств объекта в виде массива.
	 * @return
	 */
	function GetPropsArray()
	{
		$this->LoadProps();
		return $this->props;
	}

	/**
	 * Проверка существования свойства объекта.
	 * @param  $k
	 * @return bool
	 */
	function HasProp($k)
	{
		$this->LoadProps();
		return array_key_exists($k, $this->props);
	}

	/**
	 * Установка значения свойства объекта без сохранения в БД.
	 * @param  $k
	 * @param  $v
	 * @return void
	 */
	function SetTempProp($k, $v)
	{
		$this->LoadProps();
		$this->props[$k] = $v;
	}

	/**
	 * Создание нового объекта.
	 * Функция для переопределения в потомках.
	 * @static
	 * @param  $props
	 * @param bool $fill
	 * @return
	 */
	static function Create($props = array(), $fill = false)
	{
		return self::_Create($props,$fill);
	}

	/**
	 * Само создание нового объекта.
	 * @static
	 * @param  $props
	 * @param bool $fill
	 * @return
	 */
	static function _Create($props = array(), $fill = false)
	{
		$class = get_called_class();

		$tbl = $class::TABLE;
		$table	= self::GetTable($tbl);
		$prs	= array_intersect_key($props, $table['fields']);

		$res = null;
		if (is_null($prs))
		{
			$res = SQL::wquery("INSERT IGNORE INTO $tbl () VALUES ()");
		}
		else
		{
			$cleanfields = call_user_func(array($class, 'get_strip_tags_fields'));

			foreach ($cleanfields as $f)
			{
				if (isset($prs[$f])) $prs[$f] = strip_tags($prs[$f]);
			}

			$res = SQL::wquery("INSERT IGNORE INTO $tbl SET ?h", $prs);
		}

		$prs['id'] = $res->insert_id;

		$ret = new $class($res->insert_id);
		if ($fill) $ret->_FillProps($prs);

		return $ret;
	}

	function FetchFromLists()
	{
		$this->LoadProps();
		$props = func_get_args();
		$cache = RamCache::get_instance();

		$ids = array();
		$hash = array();

		foreach ($props as $prop)
		{
			if ($this->props[$prop])
			{
				$id = $this->props[$prop];

				if ($propval = $cache->get("listval_$id"))
				{
					$hash[$id] = $propval;
				}
				else
				{
					$ids[] = $this->props[$prop];
				}
			}
		}

		if ($ids)
		{
			$dbr = SQL::get_reader_instance();
			$res = $dbr->query('SELECT id, name FROM ?_lists WHERE id IN (?a)', $ids);

			while ($row = $res->fetch_hash())
			{
				$hash[$row['id']] = $row['name'];
				$cache->set('listval_'.$row['id'], $row['name']);
			}
		}

		foreach ($props as $prop)
		{
			if (!is_null($this->props[$prop]) && isset($hash[$this->props[$prop]]))
			{
				$this->props[$prop] = $hash[$this->props[$prop]];
			}
		}
	}

	/**
	 * Проверка существования объекта.
	 * @param bool $force_db
	 * @return bool
	 */
	function Exists($force_db = false)
	{
		if (!$force_db)
		{
			$this->LoadProps();
			return !is_null($this->props);
		}
		else
		{
			$props = $this->props;
			$this->props = NULL;
			$this->LoadProps();
			$ret = !is_null($this->props);
			$this->props = $props;
			return $ret;
		}
	}

	/**
	 * Уничтожение объекта.
	 * @return void
	 */
	function Drop()
	{
		$class = get_called_class();
		$tbl = $class::TABLE;

		SQL::wquery("DELETE FROM $tbl WHERE id=?", $this->id);
	}

	function FetchCity()
	{
		$this->LoadProps();
		if (!isset($this->props['city']) || !is_numeric($this->props['city'])) return;

		$details = Geo::get_city_details($this->props['city']);
		if ($details['id'] != $this->props['city']) $this->city = $details['id'];

		$this->props['city'] = $details['cityname'];
		$country = $this->props['country'] = $details['countryname'];
		if (preg_match('/^[A-Z]{2}$/', $country)) $this->props['country'] = Geo::$geostructure[$country]['name'];
		$this->props['countrycode'] = $details['country'];
	}

	/**
	 * Поиск объектов, удовлетворяющих массиву параметров.
	 * @static
	 * @param array $props
	 * @param  $order
	 * @return array
	 */
	static function Find($props=array(),$order=null)
	{
		$class = get_called_class();
		$tbl = $class::TABLE;
		$where = $params = array();

		foreach ($props as $k => $v)
		{
			if (is_array($v)) $where[] = $k.' IN (?a)';
			else $where[] = $k.'=?';

			$params[] = $v;
		}
		
		$order	= empty($order) ? '' : " ORDER BY `{$order}`";
		array_unshift($params,"SELECT * FROM $tbl".(!empty($where)? " WHERE ".implode(" AND ",$where):"").$order);

		return SQL::collection_from_query_array($class, $params);
	}

	/**
	 * Поиск одного объекта, по массиву параметров.
	 * @static
	 * @param  $props
	 * @return
	 */
	static function FindUniq($props)
	{
		$class = get_called_class();
		$tbl = $class::TABLE;
		$where = $params = array();

		foreach ($props as $k => $v)
		{
			$where[] = $k.'=?';
			$params[] = $v;
		}

		array_unshift($params,"SELECT * FROM $tbl".(!empty($where)? " WHERE ".implode(" AND ",$where):"")." LIMIT 1");
		return SQL::object_from_query_array($class, $params);
	}
}

?>